{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "185c71d4",
   "metadata": {},
   "source": [
    "# 메시지 기록(메모리) 추가하기\n",
    "\n",
    "`RunnableWithMessageHistory`를 활용하여 특정 유형의 작업(체인)에 메시지 기록을 추가하는 것은 프로그래밍에서 상당히 유용한 기능입니다.\n",
    "\n",
    "이 기능은 특히 대화형 애플리케이션 또는 복잡한 데이터 처리 작업을 구현할 때 이전 메시지의 맥락을 유지해야 할 필요가 있을 때 중요합니다.\n",
    "\n",
    "메시지 기록을 관리함으로써, 개발자는 애플리케이션의 흐름을 더 잘 제어하고, 사용자의 이전 요청에 따라 적절하게 응답할 수 있습니다.\n",
    "\n",
    "**실제 활용 예시**\n",
    "\n",
    "- 대화형 챗봇 개발: 사용자와의 대화 내역을 기반으로 챗봇의 응답을 조정할 수 있습니다.\n",
    "- 복잡한 데이터 처리: 데이터 처리 과정에서 이전 단계의 결과를 참조하여 다음 단계의 로직을 결정할 수 있습니다.\n",
    "- 상태 관리가 필요한 애플리케이션: 사용자의 이전 선택을 기억하고 그에 따라 다음 화면이나 정보를 제공할 수 있습니다.\n",
    "\n",
    "`RunnableWithMessageHistory`는 애플리케이션의 상태를 유지하고, 사용자 경험을 향상시키며, 더 정교한 응답 메커니즘을 구현할 수 있게 해주는 강력한 도구입니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "85531f5e",
   "metadata": {},
   "source": [
    "## 튜토리얼\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "d983209b",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-04-01T14:45:12.355628Z",
     "start_time": "2024-04-01T14:45:11.678242Z"
    }
   },
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "model = ChatOpenAI()\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"당신은 {ability} 에 능숙한 어시스턴트입니다. 20자 이내로 응답하세요\",\n",
    "        ),\n",
    "        # 대화 기록을 변수로 사용, history 가 MessageHistory 의 key 가 됨\n",
    "        MessagesPlaceholder(variable_name=\"history\"),\n",
    "        (\"human\", \"{input}\"),  # 사용자 입력을 변수로 사용\n",
    "    ]\n",
    ")\n",
    "runnable = prompt | model  # 프롬프트와 모델을 연결하여 runnable 객체 생성"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e57e52ec",
   "metadata": {},
   "source": [
    "메시지 기록을 관리하는 것은 대화형 애플리케이션 또는 복잡한 데이터 처리 작업에서 매우 중요합니다. 메시지 기록을 효과적으로 관리하려면 다음 두 가지 주요 요소가 필요합니다.\n",
    "\n",
    "1. **Runnable**: 주로 Retriever, Chain 과 같이 `BaseChatMessageHistory` 와 상호작용하는 `runnable` 객체입니다.\n",
    "\n",
    "2. **`BaseChatMessageHistory`의 인스턴스를 반환하는 호출 가능한 객체(callable)**: 메시지 기록을 관리하기 위한 객체입니다. 이 객체는 메시지 기록을 저장, 검색, 업데이트하는 데 사용됩니다. 메시지 기록은 대화의 맥락을 유지하고, 사용자의 이전 입력에 기반한 응답을 생성하는 데 필요합니다.\n",
    "\n",
    "메시지 기록을 구현하는 데에는 여러 방법이 있으며, [memory integrations](https://integrations.langchain.com/memory) 페이지에는 다양한 저장소 옵션과 통합 방법이 소개되어 있습니다.\n",
    "\n",
    "여기서는 두 가지 주요 방법을 살펴보겠습니다.\n",
    "\n",
    "1. **인메모리 `ChatMessageHistory` 사용**\n",
    "\n",
    "   - 이 방법은 메모리 내에서 메시지 기록을 관리합니다. 주로 개발 단계나 간단한 애플리케이션에서 사용됩니다. 인메모리 방식은 빠른 접근 속도를 제공하지만, 애플리케이션을 재시작할 때 메시지 기록이 사라지는 단점이 있습니다.\n",
    "\n",
    "2. **`RedisChatMessageHistory`를 사용하여 영구적인 저장소 활용**\n",
    "   - Redis를 사용하는 방법은 메시지 기록을 영구적으로 저장할 수 있게 해줍니다. Redis는 높은 성능을 제공하는 오픈 소스 인메모리 데이터 구조 저장소로, 분산 환경에서도 안정적으로 메시지 기록을 관리할 수 있습니다. 이 방법은 복잡한 애플리케이션 또는 장기간 운영되는 서비스에 적합합니다.\n",
    "\n",
    "메시지 기록을 관리하는 방법을 선택할 때는 애플리케이션의 요구사항, 예상되는 트래픽 양, 메시지 데이터의 중요성 및 보존 기간 등을 고려해야 합니다. 인메모리 방식은 구현이 간단하고 빠르지만, 데이터의 영구성이 요구되는 경우 Redis와 같은 영구적인 저장소를 사용하는 것이 더 적합할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6735f0d7",
   "metadata": {},
   "source": [
    "## 휘발성 대화기록: 인메모리(In-Memory)\n",
    "\n",
    "아래는 채팅 기록이 메모리에 저장되는 간단한 예시입니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0a47f6d1",
   "metadata": {},
   "source": [
    "`RunnableWithMessageHistory` 설정 매개변수\n",
    "\n",
    "- `runnable`\n",
    "- `BaseChatMessageHistory` 이거나 상속받은 객체. ex) `ChatMessageHistory`\n",
    "- `input_messages_key`: chain 을 invoke() 할때 사용자 쿼리 입력으로 지정하는 key\n",
    "- `history_messages_key`: 대화 기록으로 지정하는 key\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "9abe1aa9",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-04-01T14:43:22.819500Z",
     "start_time": "2024-04-01T14:43:22.798856Z"
    }
   },
   "outputs": [],
   "source": [
    "from langchain_community.chat_message_histories import ChatMessageHistory\n",
    "from langchain_core.chat_history import BaseChatMessageHistory\n",
    "from langchain_core.runnables.history import RunnableWithMessageHistory\n",
    "\n",
    "store = {}  # 세션 기록을 저장할 딕셔너리\n",
    "\n",
    "\n",
    "# 세션 ID를 기반으로 세션 기록을 가져오는 함수\n",
    "def get_session_history(session_ids: str) -> BaseChatMessageHistory:\n",
    "    print(session_ids)\n",
    "    if session_ids not in store:  # 세션 ID가 store에 없는 경우\n",
    "        # 새로운 ChatMessageHistory 객체를 생성하여 store에 저장\n",
    "        store[session_ids] = ChatMessageHistory()\n",
    "    return store[session_ids]  # 해당 세션 ID에 대한 세션 기록 반환\n",
    "\n",
    "\n",
    "with_message_history = (\n",
    "    RunnableWithMessageHistory(  # RunnableWithMessageHistory 객체 생성\n",
    "        runnable,  # 실행할 Runnable 객체\n",
    "        get_session_history,  # 세션 기록을 가져오는 함수\n",
    "        input_messages_key=\"input\",  # 입력 메시지의 키\n",
    "        history_messages_key=\"history\",  # 기록 메시지의 키\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e0077fb9",
   "metadata": {},
   "source": [
    "`input_messages_key`는 최신 입력 메시지로 처리될 키를 지정하고, `history_messages_key`는 이전 메시지를 추가할 키를 지정합니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ef123252",
   "metadata": {},
   "source": [
    "다음의 코드를 보면 `RunnableWithMessageHistory` 의 초기값에 `session_id` 키를 Default 로 삽입하는 것을 볼 수 있으며, 이 코드로 인하여 `RunnableWithMessageHistory` 는 대화 스레드 관리를 `session_id` 로 한다는 것을 간접적으로 알 수 있습니다.\n",
    "\n",
    "즉, 대화 스레드별 관리는 `session_id` 별로 구현함을 알 수 있습니다.\n",
    "\n",
    "참고 코드: `RunnableWithMessageHistory` 구현을 참고해 보면,\n",
    "\n",
    "```python\n",
    "if history_factory_config:\n",
    "    _config_specs = history_factory_config\n",
    "else:\n",
    "    # If not provided, then we'll use the default session_id field\n",
    "    _config_specs = [\n",
    "        ConfigurableFieldSpec(\n",
    "            id=\"session_id\",\n",
    "            annotation=str,\n",
    "            name=\"Session ID\",\n",
    "            description=\"Unique identifier for a session.\",\n",
    "            default=\"\",\n",
    "            is_shared=True,\n",
    "        ),\n",
    "    ]\n",
    "```\n",
    "\n",
    "따라서, `invoke()` 시 `config={\"configurable\": {\"session_id\": \"세션ID입력\"}}` 코드를 반드시 지정해 주어야 합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 106,
   "id": "365e124d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "abc123\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "AIMessage(content='A trigonometric function that relates the adjacent side of a right triangle to its hypotenuse.', response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 146, 'total_tokens': 166}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3bc1b5746c', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 106,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    # 수학 관련 질문 \"코사인의 의미는 무엇인가요?\"를 입력으로 전달합니다.\n",
    "    {\"ability\": \"math\", \"input\": \"What does cosine mean?\"},\n",
    "    # 설정 정보로 세션 ID \"abc123\"을 전달합니다.\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d39629e",
   "metadata": {},
   "source": [
    "같은 `session_id` 를 입력하면 이전 대화 스레드의 내용을 가져오기 때문에 이어서 대화가 가능합니다!\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 104,
   "id": "531ea924",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "abc123\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "AIMessage(content='직각삼각형의 인접변과 빗변 간의 관계를 나타내는 삼각함수입니다.', response_metadata={'token_usage': {'completion_tokens': 43, 'prompt_tokens': 90, 'total_tokens': 133}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3bc1b5746c', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 104,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 메시지 기록을 포함하여 호출합니다.\n",
    "with_message_history.invoke(\n",
    "    # 능력과 입력을 설정합니다.\n",
    "    {\"ability\": \"math\", \"input\": \"이전의 내용을 한글로 답변해 주세요.\"},\n",
    "    # 설정 옵션을 지정합니다.\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce0cc35c",
   "metadata": {},
   "source": [
    "하지만 다른 `session_id` 를 지정하면 대화기록이 없기 때문에 답변을 제대로 수행하지 못합니다.\n",
    "\n",
    "(아래의 예시는 `session_id`: `def234` 는 존재하지 않기 때문에 엉뚱한 답변을 하는 것을 확인할 수 있습니다)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "34e5cac7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "def234\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "AIMessage(content='수학에 능숙한 어시스턴트입니다.', response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 58, 'total_tokens': 77}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3bc1b5746c', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 새로운 session_id로 인해 이전 대화 내용을 기억하지 않습니다.\n",
    "with_message_history.invoke(\n",
    "    # 수학 능력과 입력 메시지를 전달합니다.\n",
    "    {\"ability\": \"math\", \"input\": \"이전의 내용을 한글로 답변해 주세요\"},\n",
    "    # 새로운 session_id를 설정합니다.\n",
    "    config={\"configurable\": {\"session_id\": \"def234\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0eda900a",
   "metadata": {},
   "source": [
    "메시지 기록을 추적하는 데 사용되는 구성 매개변수는 `ConfigurableFieldSpec` 객체의 리스트를 `history_factory_config` 매개변수로 전달하여 사용자 정의할 수 있습니다.\n",
    "\n",
    "이렇게 `history_factory_config` 를 새로 설정하게 되면 기존 `session_id` 설정을 덮어쓰게 됩니다.\n",
    "\n",
    "아래 예시에서는 `user_id`와 `conversation_id`라는 두 가지 매개변수를 사용합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 109,
   "id": "fcc58e17",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import ConfigurableFieldSpec\n",
    "\n",
    "store = {}  # 빈 딕셔너리를 초기화합니다.\n",
    "\n",
    "\n",
    "def get_session_history(user_id: str, conversation_id: str) -> BaseChatMessageHistory:\n",
    "    # 주어진 user_id와 conversation_id에 해당하는 세션 기록을 반환합니다.\n",
    "    if (user_id, conversation_id) not in store:\n",
    "        # 해당 키가 store에 없으면 새로운 ChatMessageHistory를 생성하여 저장합니다.\n",
    "        store[(user_id, conversation_id)] = ChatMessageHistory()\n",
    "    return store[(user_id, conversation_id)]\n",
    "\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    runnable,\n",
    "    get_session_history,\n",
    "    input_messages_key=\"input\",\n",
    "    history_messages_key=\"history\",\n",
    "    history_factory_config=[  # 기존의 \"session_id\" 설정을 대체하게 됩니다.\n",
    "        ConfigurableFieldSpec(\n",
    "            id=\"user_id\",  # get_session_history 함수의 첫 번째 인자로 사용됩니다.\n",
    "            annotation=str,\n",
    "            name=\"User ID\",\n",
    "            description=\"사용자의 고유 식별자입니다.\",\n",
    "            default=\"\",\n",
    "            is_shared=True,\n",
    "        ),\n",
    "        ConfigurableFieldSpec(\n",
    "            id=\"conversation_id\",  # get_session_history 함수의 두 번째 인자로 사용됩니다.\n",
    "            annotation=str,\n",
    "            name=\"Conversation ID\",\n",
    "            description=\"대화의 고유 식별자입니다.\",\n",
    "            default=\"\",\n",
    "            is_shared=True,\n",
    "        ),\n",
    "    ],\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 111,
   "id": "5c3974ca",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='Trigonometric function relating the adjacent side to the hypotenuse in a right triangle.', response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 46, 'total_tokens': 64}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_b28b39ffa8', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 111,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    # 능력(ability)과 입력(input)을 포함한 딕셔너리를 전달합니다.\n",
    "    {\"ability\": \"math\", \"input\": \"what is cosine?\"},\n",
    "    # 설정(config) 딕셔너리를 전달합니다.\n",
    "    config={\"configurable\": {\"user_id\": \"123\", \"conversation_id\": \"abc\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 114,
   "id": "d45e28d5",
   "metadata": {},
   "outputs": [],
   "source": [
    "answer = with_message_history.invoke(\n",
    "    # 능력(ability)과 입력(input)을 포함한 딕셔너리를 전달합니다.\n",
    "    {\"ability\": \"math\", \"input\": \"이전의 답변을 한글로 작성해 주세요\"},\n",
    "    # 설정(config) 딕셔너리를 전달합니다.\n",
    "    config={\"configurable\": {\"user_id\": \"123\", \"conversation_id\": \"abc\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e531e609",
   "metadata": {},
   "source": [
    "## 다양한 Key를 사용한 Runnable 을 사용한 예시\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04f9d753",
   "metadata": {},
   "source": [
    "### Messages 객체를 입력, dict 형태의 출력\n",
    "\n",
    "메시지를 입력으로 받고 딕셔너리를 출력으로 반환하는 경우입니다.\n",
    "\n",
    "- [중요]: `input_messages_key`=\"input\" 을 생략합니다. 그럼 입력으로 Message 객체를 넣도록 설정하게 됩니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 117,
   "id": "676e8b5d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'output_message': AIMessage(content='In mathematics, the cosine of an angle in a right-angled triangle is defined as the ratio of the length of the side adjacent to the angle to the length of the hypotenuse. It is one of the basic trigonometric functions and is abbreviated as cos.', response_metadata={'token_usage': {'completion_tokens': 54, 'prompt_tokens': 14, 'total_tokens': 68}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3bc1b5746c', 'finish_reason': 'stop', 'logprobs': None})}"
      ]
     },
     "execution_count": 117,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_core.messages import HumanMessage\n",
    "from langchain_core.runnables import RunnableParallel\n",
    "\n",
    "# chain 생성\n",
    "chain = RunnableParallel({\"output_message\": ChatOpenAI()})\n",
    "\n",
    "\n",
    "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n",
    "    # 세션 ID에 해당하는 대화 기록이 저장소에 없으면 새로운 ChatMessageHistory를 생성합니다.\n",
    "    if session_id not in store:\n",
    "        store[session_id] = ChatMessageHistory()\n",
    "    # 세션 ID에 해당하는 대화 기록을 반환합니다.\n",
    "    return store[session_id]\n",
    "\n",
    "\n",
    "# 체인에 대화 기록 기능을 추가한 RunnableWithMessageHistory 객체를 생성합니다.\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    chain,\n",
    "    get_session_history,\n",
    "    # 입력 메시지의 키를 \"input\"으로 설정합니다.(생략시 Message 객체로 입력)\n",
    "    # input_messages_key=\"input\",\n",
    "    # 출력 메시지의 키를 \"output_message\"로 설정합니다. (생략시 Message 객체로 출력)\n",
    "    output_messages_key=\"output_message\",\n",
    ")\n",
    "\n",
    "# 주어진 메시지와 설정으로 체인을 실행합니다.\n",
    "with_message_history.invoke(\n",
    "    # 혹은 \"what is the definition of cosine?\" 도 가능\n",
    "    [HumanMessage(content=\"what is the definition of cosine?\")],\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 72,
   "id": "2324aea1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'output_message': AIMessage(content='코사인 함수는 주어진 각도의 코사인 값을 반환하는 삼각함수로, 직각삼각형에서 인접 변의 길이와 빗변의 길이의 비율을 나타냅니다. 수학적으로, 각도 θ에 대한 코사인 값은 그 각도를 가지는 직각삼각형의 인접 변의 길이를 빗변의 길이로 나눈 것으로 정의됩니다. 코사인 함수는 일반적으로 \"cos\"로 표기되며, 각도는 일반적으로 라디안 단위로 표현됩니다.', response_metadata={'token_usage': {'completion_tokens': 187, 'prompt_tokens': 1020, 'total_tokens': 1207}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3bc1b5746c', 'finish_reason': 'stop', 'logprobs': None})}"
      ]
     },
     "execution_count": 72,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    # 이전의 답변에 대하여 한글로 답변을 재요청합니다.\n",
    "    [HumanMessage(content=\"이전의 내용을 한글로 답변해 주세요!\")],\n",
    "    # 설정 옵션을 딕셔너리 형태로 전달합니다.\n",
    "    config={\"configurable\": {\"session_id\": \"abc123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "390c2a4e",
   "metadata": {},
   "source": [
    "### Messages 객체를 입력, Messages 객체의 출력\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d13d7c8",
   "metadata": {},
   "source": [
    "- [중요]: `output_messages_key`=\"output_message\" 을 생략합니다. 그럼 출력으로 Message 객체를 반환합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 118,
   "id": "07d90e3d",
   "metadata": {},
   "outputs": [],
   "source": [
    "with_message_history = RunnableWithMessageHistory(\n",
    "    ChatOpenAI(),  # ChatOpenAI 언어 모델을 사용합니다.\n",
    "    get_session_history,  # 대화 세션 기록을 가져오는 함수를 지정합니다.\n",
    "    # 입력 메시지의 키를 \"input\"으로 설정합니다.(생략시 Message 객체로 입력)\n",
    "    # input_messages_key=\"input\",\n",
    "    # 출력 메시지의 키를 \"output_message\"로 설정합니다. (생략시 Message 객체로 출력)\n",
    "    # output_messages_key=\"output_message\",\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 119,
   "id": "91fdb0e5",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Error in RootListenersTracer.on_llm_end callback: KeyError('input')\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "AIMessage(content='코사인은 삼각함수 중 하나로, 직각삼각형에서의 각도에 대해 대변과 빗변의 비율을 나타내는 값입니다. 코사인 함수는 주어진 각도에 대해 코사인 값을 계산해주는 함수이며, 삼각형의 변 길이를 이용해 각도를 구하거나 각도를 이용해 변 길이를 구하는 데 사용됩니다. 코사인은 삼각형의 오른쪽 변에 인접한 변과 빗변의 비율을 의미하며, 각도가 90도일 때 1이 되는 값입니다.', response_metadata={'token_usage': {'completion_tokens': 191, 'prompt_tokens': 24, 'total_tokens': 215}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_b28b39ffa8', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 119,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    # 이전의 답변에 대하여 한글로 답변을 재요청합니다.\n",
    "    [HumanMessage(content=\"코사인의 의미는 무엇인가요?\")],\n",
    "    # 설정 옵션을 딕셔너리 형태로 전달합니다.\n",
    "    config={\"configurable\": {\"session_id\": \"def123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5fa77902",
   "metadata": {},
   "source": [
    "#### 모든 메시지 입력과 출력을 위한 단일 키를 가진 Dict\n",
    "\n",
    "모든 입력 메시지와 출력 메시지에 대해 단일 키를 사용하는 방식입니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b38a016f",
   "metadata": {},
   "source": [
    "- `itemgetter(\"input_messages\")`를 사용하여 입력 메시지를 추출합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 120,
   "id": "ee02f522",
   "metadata": {},
   "outputs": [],
   "source": [
    "from operator import itemgetter\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    # \"input_messages\" 키를 사용하여 입력 메시지를 가져와 ChatOpenAI()에 전달합니다.\n",
    "    itemgetter(\"input_messages\") | ChatOpenAI(),\n",
    "    get_session_history,  # 세션 기록을 가져오는 함수입니다.\n",
    "    input_messages_key=\"input_messages\",  # 입력 메시지의 키를 지정합니다.\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 121,
   "id": "7b0f2b18",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='코사인은 삼각함수 중 하나로, 직각삼각형에서의 각도에 대해 대변과 빗변의 비율을 나타내는 함수입니다. 각도가 주어졌을 때 코사인 값은 삼각형에서 밑변과 빗변의 길이를 나타내는 비율을 의미합니다. 즉, 코사인은 각도에 대한 삼각비로서, 삼각형의 변의 길이와 각도 사이의 관계를 나타내는 중요한 개념입니다.', response_metadata={'token_usage': {'completion_tokens': 172, 'prompt_tokens': 24, 'total_tokens': 196}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3bc1b5746c', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 121,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    {\"input_messages\": \"코사인의 의미는 무엇인가요?\"},\n",
    "    # 설정 옵션을 딕셔너리 형태로 전달합니다.\n",
    "    config={\"configurable\": {\"session_id\": \"xyz123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ebd470fb",
   "metadata": {},
   "source": [
    "## 영구 저장소(Persistent storage)\n",
    "\n",
    "영구 저장소(Persistent storage)는 프로그램이 종료되거나 시스템이 재부팅되더라도 데이터를 유지하는 저장 메커니즘을 말합니다. 이는 데이터베이스, 파일 시스템, 또는 기타 비휘발성 저장 장치를 통해 구현될 수 있습니다.\n",
    "\n",
    "영구 저장소는 애플리케이션의 상태를 저장하고, 사용자 설정을 유지하며, **장기간 데이터를 보존하는 데 필수적**입니다. 이를 통해 프로그램은 이전 실행에서 중단된 지점부터 다시 시작할 수 있으며, **사용자는 데이터 손실 없이 작업을 계속** 할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56d66c80",
   "metadata": {},
   "source": [
    "- `RunnableWithMessageHistory`는 `get_session_history` 호출 가능 객체가 채팅 메시지 기록을 어떻게 검색하는지에 대해 독립적입니다.\n",
    "- 로컬 파일 시스템을 사용하는 예제는 [여기](https://github.com/langchain-ai/langserve/blob/main/examples/chat_with_persistence_and_user/server.py)를 참조하세요.\n",
    "- 아래에서는 **Redis** 를 사용하는 방법을 보여줍니다. 다른 제공자를 사용하여 채팅 메시지 기록을 구현하는 방법은 [memory integrations](https://integrations.langchain.com/memory) 페이지를 확인하세요.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "818607a5",
   "metadata": {},
   "source": [
    "### Redis 설치\n",
    "\n",
    "Redis가 설치되어 있지 않다면 먼저 설치해야 합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b577c06b",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install -qU redis"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b35229d",
   "metadata": {},
   "source": [
    "### Redis 서버 구동\n",
    "\n",
    "기존에 연결할 Redis 배포가 없는 경우, 로컬 Redis Stack 서버를 시작합니다.\n",
    "\n",
    "다음은 Docker 로 Redis 서버를 구동하는 명령어입니다.\n",
    "\n",
    "```bash\n",
    "docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9c2a12a6",
   "metadata": {},
   "source": [
    "`REDIS_URL` 변수에 Redis 데이터베이스의 연결 URL을 할당합니다.\n",
    "\n",
    "- URL은 `\"redis://localhost:6379/0\"`로 설정되어 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 122,
   "id": "c99a3ad4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Redis 서버의 URL을 지정합니다.\n",
    "REDIS_URL = \"redis://localhost:6379/0\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67bd11ee",
   "metadata": {},
   "source": [
    "**LangSmith 추적 설정**\n",
    "\n",
    "추적을 위한 LangSmith 를 설정합니다. LangSmith는 필수적인 것은 아니지만, 도움이 될 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 123,
   "id": "fc02f43c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from dotenv import load_dotenv\n",
    "import os\n",
    "\n",
    "load_dotenv()\n",
    "\n",
    "# LANGCHAIN_TRACING_V2 환경 변수를 \"true\"로 설정합니다.\n",
    "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
    "# LANGCHAIN_PROJECT 설정\n",
    "os.environ[\"LANGCHAIN_PROJECT\"] = \"RunnableWithMessageHistory\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4654a294",
   "metadata": {},
   "source": [
    "메시지 기록 구현을 업데이트하려면 새로운 호출 가능한 객체를 정의하고, 이번에는 RedisChatMessageHistory의 인스턴스를 반환하면 됩니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 124,
   "id": "42346b0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.chat_message_histories import RedisChatMessageHistory\n",
    "\n",
    "\n",
    "def get_message_history(session_id: str) -> RedisChatMessageHistory:\n",
    "    # 세션 ID를 기반으로 RedisChatMessageHistory 객체를 반환합니다.\n",
    "    return RedisChatMessageHistory(session_id, url=REDIS_URL)\n",
    "\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(\n",
    "    runnable,  # 실행 가능한 객체\n",
    "    get_message_history,  # 메시지 기록을 가져오는 함수\n",
    "    input_messages_key=\"input\",  # 입력 메시지의 키\n",
    "    history_messages_key=\"history\",  # 기록 메시지의 키\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5670c0fe",
   "metadata": {},
   "source": [
    "이전과 동일한 방식으로 호출할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 125,
   "id": "084eead7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='It is a trigonometric function that gives the ratio of the adjacent side to the hypotenuse in a right triangle.', response_metadata={'token_usage': {'completion_tokens': 25, 'prompt_tokens': 47, 'total_tokens': 72}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_b28b39ffa8', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 125,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    # 수학 관련 질문 \"코사인의 의미는 무엇인가요?\"를 입력으로 전달합니다.\n",
    "    {\"ability\": \"math\", \"input\": \"What does cosine mean?\"},\n",
    "    # 설정 옵션으로 세션 ID를 \"redis123\" 로 지정합니다.\n",
    "    config={\"configurable\": {\"session_id\": \"redis123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e5baf7d",
   "metadata": {},
   "source": [
    "동일한 `session_id`를 사용하여 두 번째 호출을 수행합니다. 이번에는 이전의 답변을 한글로 답변해 달라는 요청을 해보겠습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 126,
   "id": "068ab370",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='인접변을 빗변으로 나눈 비율을 나타내는 삼각함수입니다.', response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 98, 'total_tokens': 132}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_b28b39ffa8', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 126,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    # 이전 답변에 대한 한글 번역을 요청합니다.\n",
    "    {\"ability\": \"math\", \"input\": \"이전의 답변을 한글로 번역해 주세요.\"},\n",
    "    # 설정 값으로 세션 ID를 \"foobar\"로 지정합니다.\n",
    "    config={\"configurable\": {\"session_id\": \"redis123\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41dc695e",
   "metadata": {},
   "source": [
    "이번에는 다른 `session_id`를 사용하여 질문을 하겠습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 127,
   "id": "14c277bf",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='수학에 능숙한 어시스턴트입니다.', response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 60, 'total_tokens': 79}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3bc1b5746c', 'finish_reason': 'stop', 'logprobs': None})"
      ]
     },
     "execution_count": 127,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with_message_history.invoke(\n",
    "    # 이전 답변에 대한 한글 번역을 요청합니다.\n",
    "    {\"ability\": \"math\", \"input\": \"이전의 답변을 한글로 번역해 주세요.\"},\n",
    "    # 설정 값으로 세션 ID를 \"redis456\"로 지정합니다.\n",
    "    config={\"configurable\": {\"session_id\": \"redis456\"}},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e4f00b2",
   "metadata": {},
   "source": [
    "마지막 답변은 이전 대화기록이 없으므로, 제대로 된 답변을 받을 수 없습니다.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
